package de.uni.muenster.idw2.location.bluetooth.service.fingerprinting.images;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Overlays two range images.
 * 
 * @author Eva Peters
 *
 */
public class PositionRangeArrayImageOverlay extends AbstractImage
{
	/**
	 * Already overlayed position range images.
	 */
	private static Map<RangeArrayImage, List<RangeArrayImage>> alreadyOverlayedPositionRangeImages = new HashMap<RangeArrayImage, List<RangeArrayImage>>();
	
	/**
	 * The first range image.
	 */
	private RangeArrayImage rangeImage1;
	
	/**
	 * The second range image.
	 */
	private RangeArrayImage rangeImage2;
	
	/**
	 * Constructor.
	 * 
	 * @param rangeImage1 The first range image.
	 * @param rangeImage2 The second range image.
	 * @throws IncompatibleImagesException If the images are incompatible.
	 */
	public PositionRangeArrayImageOverlay(RangeArrayImage rangeImage1,
							 RangeArrayImage rangeImage2) throws IncompatibleImagesException
	{
		String incompatibleMessage = "The images cannot be overlayed because ";
		
		int width1 = rangeImage1.getWidth();
		int width2 = rangeImage2.getWidth();
		if(width1 != width2)
		{
			throw new IncompatibleImagesException(incompatibleMessage + "the widths are different ('" + width1 + "' and '" + width2 + "').");
		}
		this.width = width1;
		
		int height1 = rangeImage1.getHeight();
		int height2 = rangeImage2.getHeight();
		if(width1 != width2)
		{
			throw new IncompatibleImagesException(incompatibleMessage + "the heights are different ('" + height1 + "' and '" + height2 + "').");
		}
		this.height = height1;
				
		this.arrayBitmap = new ArrayBitmap(width, height);		
		this.rangeImage1 = rangeImage1;
		this.rangeImage2 = rangeImage2;
	}
	
	/**
	 * Overlays both images.
	 * 
	 * There are three cases:
	 * - If the image shares marked pixels, 
	 *   these pixels are marked.
	 * - Otherwise, if there are marked pixels which touch each other, 
	 *   these pixels are marked.
	 * - Otherwise, if they do not share marked pixels 
	 *   and if there are not any touching marked pixels, 
	 *   the nearest pixels are marked.
	 *   
	 * @throws IOException If the image cannot be written.
	 */
	public void overlay() throws IOException
	{
		overlayRangeImages();
		addAlreadyOverlayedPositionRangeImages(rangeImage1, rangeImage2);
	}
	
	/**
	 * Adds both position range images to the map of already overlayed range images.
	 * 
	 * @param rangeImage1 The first position range image.
	 * @param rangeImage2 The second position range image.
	 */
	private static void addAlreadyOverlayedPositionRangeImages(RangeArrayImage rangeImage1, RangeArrayImage rangeImage2)
	{
		List<RangeArrayImage> rangeImages = alreadyOverlayedPositionRangeImages.get(rangeImage1);
		if(rangeImages != null)
		{
			rangeImages.add(rangeImage2);
		}
		else
		{
			rangeImages = new ArrayList<RangeArrayImage>();
			rangeImages.add(rangeImage2);
			alreadyOverlayedPositionRangeImages.put(rangeImage1, rangeImages);
		}
	}
	
	/**
	 * Overlays both range images.
	 */
	private void overlayRangeImages()
	{
		int markedDistance = -2; //Initialize
		
		int endDistance = width > height ? width : height;
		
		for(int distance = 0; distance < endDistance; distance++)
		{
			boolean distanceMarked = false;
			if(distance == (markedDistance + 1))
			{
				break;
			}
			
			for(int y = 0; y < height; y++)
			{
				for(int x = 0; x < width; x++)
				{
					boolean marked = false; //Initialize
					
					if((y - distance) >= 0)
					{
						//NorthWest
						if((x - distance) >= 0)
						{
							int x_NorthWest = x - distance;
							int y_NorthWest = y - distance;
							marked = overlayTwoPixels(x, y, x_NorthWest, y_NorthWest);
							distanceMarked = marked ? marked : distanceMarked;
						}
						
						//NorthEast
						if((x + distance) < width)
						{
							int x_NorthEast = x + distance;
							int y_NorthEast = y - distance;
							marked = overlayTwoPixels(x, y, x_NorthEast, y_NorthEast);
							distanceMarked = marked ? marked : distanceMarked;
						}
						
						//North
						int x_North = x;
						int y_North = y - distance;
						marked = overlayTwoPixels(x, y, x_North, y_North);
						distanceMarked = marked ? marked : distanceMarked;
					}
					
					//West
					if((x - distance) >= 0)
					{
						int x_West = x - distance;
						int y_West = y;
						marked = overlayTwoPixels(x, y, x_West, y_West);
						distanceMarked = marked ? marked : distanceMarked;
					}
					
					//East
					if((x + distance) < width)
					{
						int x_East = x + distance;
						int y_East = y;
						marked = overlayTwoPixels(x, y, x_East, y_East);
						distanceMarked = marked ? marked : distanceMarked;
					}
					
					//Here
					marked = overlayTwoPixels(x, y, x, y);
					distanceMarked = marked ? marked : distanceMarked;
				}
			}
			
			if(distanceMarked)
			{
				endDistance = distance;
				markedDistance = distance;
			}
		}
	}
	
	/**
	 * Overlays two pixels of the range image 1 and range image 2.
	 * 
	 * If they are both marked, these pixels are marked in the range image overlay.
	 * 
	 * @param x_1 X position of the first pixel in the range image 1 (to the right).
	 * @param y_1 Y position of the first pixel in the range image 1 (to the right).
	 * @param x_2 X position of the second pixel in the range image 2 (to the right).
	 * @param y_2 Y position of the second pixel in the range image 2 (to the right).
	 * @return True if pixels were marked in the range image overlay.
	 */
	private boolean overlayTwoPixels(int x_1, int y_1, int x_2, int y_2)
	{
		boolean isPixel1_marked = rangeImage1.isPixelMarked(x_1, y_1);
		boolean isPixel2_marked = rangeImage2.isPixelMarked(x_2, y_2);
		
		if(isPixel1_marked && isPixel2_marked)
		{
			double x_average = (double)(x_1 + x_2)/ (double)2;
			double y_average = (double)(y_1 + y_2)/ (double)2;
			
			int x_average_int = (int)x_average;
			int y_average_int = (int)y_average;
			
			int x_A = -1; //initialize
			int y_A = -1; //initialize
			int x_B = -1; //initialize
			int y_B = -1; //initialize
			
			if(x_average - (double)x_average_int > 0)
			{
				x_A = (x_1 < x_2) ? ((int)x_average - 1) : ((int)x_average + 1);
				x_B = (x_2 < x_1) ? ((int)x_average - 1) : ((int)x_average + 1);
				
				if(y_average - (double)y_average_int > 0)
				{
					y_A = (y_1 < y_2) ? ((int)y_average - 1) : ((int)y_average + 1);
					y_B = (y_2 < y_1) ? ((int)y_average - 1) : ((int)y_average + 1);
				}
				//y = integer
				else
				{
					y_A = (int)y_average;
					y_B = (int)y_average;
				}
			}
			//x = integer
			else
			{
				x_A = (int)x_average;
				x_B = (int)x_average;
				
				if(y_average - (double)y_average_int > 0)
				{
					y_A = (int)y_average + 1;
					y_B = (int)y_average - 1;
				}
				//y = integer
				else
				{
					y_A = (int)y_average;
					y_B = (int)y_average;
				}
			}
			
			markPixel(x_A, y_A);
			markPixel(x_B, y_B);
			return true;
		}
		return false;
	}

	/**
	 * Checks if both range image have already been overlayed.
	 * 
	 * @param dongleName1 The dongle name of the first range image.
	 * @param dongleName2 The dongle name of the second range image.
	 * @return True if they have already been overlayed. Otherwise false.
	 */
	public static boolean isOverlayedForDeterminingPosition(String floor1,
									  String dongleName1,
									  String floor2,
									  String dongleName2)
	{
		for(Map.Entry<RangeArrayImage, List<RangeArrayImage>> images : alreadyOverlayedPositionRangeImages.entrySet())
		{
			RangeArrayImage rangeImageA = images.getKey();
			String dongleNameA = rangeImageA.getDongleName();
			
			List<RangeArrayImage> rangeImages = images.getValue();
			for(RangeArrayImage rangeImageB : rangeImages)
			{
				String dongleNameB = rangeImageB.getDongleName();
				
				if((dongleNameA.equalsIgnoreCase(dongleName1) && 
				    dongleNameB.equalsIgnoreCase(dongleName2)) ||
				   (dongleNameA.equalsIgnoreCase(dongleName2) && 
				    dongleNameB.equalsIgnoreCase(dongleName1)))
				{
					return true;
				}
			}
		}

		return false;
	}
	
	/**
	 * Returns the positions of the marked pixels in the overlay.
	 * 
	 * @return The positions of the marked pixels.
	 */
	public List<PixelPosition> getPositionsOfMarkedPixels()
	{
		List<PixelPosition> positions = new ArrayList<PixelPosition>();
		
		for(int y = 0; y < height; y++)
		{
			for(int x = 0; x < width; x++)
			{
				boolean marked = isPixelMarked(x, y);
				if(marked)
				{
					positions.add(new PixelPosition((double) x, (double) y));
				}
			}
		}
		
		return positions;
	}
}
